package com.google.precinct.client.mapper;

import java.util.ArrayList;
import java.util.List;

import com.allen_sauer.gwt.log.client.Log;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.event.MapClickHandler;
import com.google.gwt.maps.client.event.MapZoomEndHandler;
import com.google.gwt.maps.client.geocode.Geocoder;
import com.google.gwt.maps.client.geocode.LocationCallback;
import com.google.gwt.maps.client.geocode.Placemark;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.geom.LatLngBounds;
import com.google.gwt.maps.client.geom.Point;
import com.google.gwt.maps.client.geom.Size;
import com.google.gwt.maps.client.overlay.Icon;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.maps.client.overlay.Overlay;
import com.google.gwt.maps.client.overlay.Polygon;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineHyperlink;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.precinct.client.BreadCrumbManager;
import com.google.precinct.client.UIUtils;
import com.google.precinct.client.Util;
import com.google.precinct.common.AddressInfo;
import com.google.precinct.common.PrecinctService;
import com.google.precinct.common.PrecinctServiceAsync;

public class AddressMappingBLPanel extends Composite {

	private static AddressMappingBLPanelUiBinder uiBinder = GWT
			.create(AddressMappingBLPanelUiBinder.class);

	interface AddressMappingBLPanelUiBinder extends
			UiBinder<Widget, AddressMappingBLPanel> {
	}

	@UiField
	InlineLabel mappingAddress;

	@UiField
	InlineLabel voterCount;

	@UiField
	InlineLabel clusterCount;

	@UiField
	InlineLabel userUsageText;

	@UiField
	InlineHyperlink submitLocation;

	@UiField
	Label errorLabel;

	@UiField
	InlineLabel loadingLabel;

	@UiField
	Image spinnerImage;

	private final MapRootPage ctr;
	private final AddressInfo addressInfo;
	private final MapWidget maps;
	private Marker precinctMarker;
	private boolean finalizingLocation;
	private MapClickHandler clickHandler;
	private MapZoomEndHandler zoomHandler;
	private final BreadCrumbManager breadCrumb;
	private final List<Overlay> forZoomClearing = new ArrayList<Overlay>();


	public AddressMappingBLPanel(MapRootPage ctr,
			AddressInfo addressInfo,
			MapWidget maps, BreadCrumbManager breadCrumb) {
		this.ctr = ctr;
		this.addressInfo = addressInfo;
		this.maps = maps;
		this.breadCrumb = breadCrumb;
		initWidget(uiBinder.createAndBindUi(this));

		mappingAddress.setText(addressInfo.getDisplayAddress());
		voterCount.setText(NumberFormat.getDecimalFormat().format(
				addressInfo.getVotersCount()));
		clusterCount.setText(NumberFormat.getDecimalFormat().format(
				addressInfo.getClusterCount()));

		initializeMaps();
		Util.track("/PrecinctFinder/08-AddressMapping");
	}

	private void initializeMaps() {
		clickHandler = new MapClickHandler() {

			@Override
			public void onClick(MapClickEvent event) {
				if (maps.getZoomLevel() < 15) {
					UIUtils.error(errorLabel,
							"Zoom In closer to drop the precinct marker better.");
					return;
				}
//				if (precinctMarker != null) {
//					maps.removeOverlay(precinctMarker);
//				}
				precinctMarker = new Marker(event.getLatLng());
				precinctMarker.setDraggingEnabled(true);
				Icon precinctIcon = precinctMarker.getIcon();
				precinctIcon.setImageURL("http://chart.apis.google.com/chart?chst=d_map_xpin_icon&chld=pin_sright|civic-building|C3593C");
				precinctIcon.setShadowURL("http://chart.apis.google.com/chart?chst=d_map_xpin_shadow&chld=pin_sright|civic-building|C3593C");
				precinctIcon.setIconSize(Size.newInstance(23, 33));
				precinctIcon.setShadowSize(Size.newInstance(42, 36));
				precinctIcon.setIconAnchor(Point.newInstance(0, 33));
				maps.addOverlay(precinctMarker);
				submitLocation.removeStyleName("disabled");
				userUsageText.setText("After confirming the location is correct, click on the Submit button.");
			}

		};
		maps.addMapClickHandler(clickHandler);

		zoomHandler = new MapZoomEndHandler() {

			@Override
			public void onZoomEnd(MapZoomEndEvent event) {
				if (event.getNewZoomLevel() >= 15) {
					for (Overlay ov : forZoomClearing)
						maps.removeOverlay(ov);
					forZoomClearing.clear();
				}
			}
		};
		maps.addMapZoomEndHandler(zoomHandler);

		startSmartSearch();
	}

	private void startSmartSearch() {
		spinnerImage.setVisible(true);
		loadingLabel.setText("Attempting to Auto-locate the address...");

		Geocoder coder = new Geocoder();
		final String searchAddress = addressInfo.getCity() + "," +
				addressInfo.getProvince() + ", Philippines";
		coder.setBaseCountryCode("ph");
		coder.setViewport(LatLngBounds.newInstance(
				LatLng.newInstance(4.127285, 114.609375 ),
				LatLng.newInstance(21.207459, 127.705078 )));
		coder.getLocations(cleanAddress(searchAddress), new SmartLocationCallback(maps,
				cleanAddress(addressInfo.getDisplayAddress()), null, null, new Runnable() {
			@Override
			public void run() {
				spinnerImage.setVisible(false);
				loadingLabel.setText("");
			}
		}, 4, this.forZoomClearing, errorLabel));
	}

	private String cleanAddress(String searchAddress) {
		return searchAddress.replace("Elem.", "Elementary");
	}

	static public class SmartLocationCallback implements LocationCallback {

		private final MapWidget maps2;
		private final Placemark bestLocation;
		private final LatLngBounds bounds;
		private final Runnable done;
		private final String nextAddress;
		private final int iteration;
		private final List<Overlay> forZoomClearing2;
		private final Label errorLabel2;

		public SmartLocationCallback(MapWidget maps, String nextAddress,
				Placemark bestLocation,
				LatLngBounds bounds, Runnable done, int iteration,
				List<Overlay> forZoomClearing,
				Label errorLabel) {
					maps2 = maps;
					this.nextAddress = nextAddress;
					this.bestLocation = bestLocation;
					this.bounds = bounds;
					this.done = done;
					this.iteration = iteration;
					forZoomClearing2 = forZoomClearing;
					errorLabel2 = errorLabel;
		}

		@Override
		public void onSuccess(JsArray<Placemark> locations) {
			int bestLocation = -1;
			Log.debug("Found:" + locations.length());
			for (int i = 0; i < locations.length(); i++) {
				Log.debug("Pre-Acc:" + locations.get(i).getAccuracy());
				Placemark loc = locations.get(i);
				// Bounds Check
				if (bounds != null) {
					if (bounds.getSouthWest().getLatitude() >= loc.getPoint().getLatitude() ||
							bounds.getSouthWest().getLongitude() >= loc.getPoint().getLongitude() ||
							bounds.getNorthEast().getLatitude() < loc.getPoint().getLatitude() ||
							bounds.getNorthEast().getLongitude() < loc.getPoint().getLongitude()) {
						Log.debug("Out of Bounds.");
						continue;
					}
				}
				if (bestLocation == -1 || loc.getAccuracy() >
						locations.get(bestLocation).getAccuracy()) {
					bestLocation = i;
				}
			}
			final LatLngBounds finalBounds;
			if (bestLocation != -1) {
				Placemark best = locations.get(bestLocation);
				double lat = best.getPoint().getLatitude();
				double lng = best.getPoint().getLongitude();
				double off = 0.3;
				if (best.getAccuracy() == 4) {
					off = 0.2;
				} else if (best.getAccuracy() == 5) {
					off = 0.1;
				} else if (best.getAccuracy() == 6) {
					off = 0.05;
				} else if (best.getAccuracy() == 7) {
					off = 0.02;
				} else if (best.getAccuracy() == 8) {
					off = 0.008;
				} else if (best.getAccuracy() == 9) {
					off = 0.004;
				}
				for (int i = 0; i < 3; i++) {
					double offNew = (off / 6) * (6 - i);
					double transNew = (.6 / 5) * (5 - i);
					Overlay ov1 = new Polygon(
							new LatLng[] {
									LatLng.newInstance(lat - offNew, lng - offNew),
									LatLng.newInstance(lat - offNew, lng + offNew),
									LatLng.newInstance(lat + offNew, lng + offNew),
									LatLng.newInstance(lat + offNew, lng - offNew),
									LatLng.newInstance(lat - offNew, lng - offNew)
							}
							, "#FFF", 2, transNew, "", 0);
					maps2.addOverlay(ov1);
					forZoomClearing2.add(ov1);
				}
				if (bounds == null) {
					finalBounds = LatLngBounds.newInstance(
							LatLng.newInstance(lat - off, lng - off),
							LatLng.newInstance(lat + off, lng + off)
							);
				} else {
					finalBounds = bounds;
				}
				if (best.getAccuracy() >= 6) {
					// Plot a point too if its close enough
					Marker guess = new Marker(best.getPoint());
					guess.setDraggingEnabled(false);
					Icon precinctIcon = guess.getIcon();
					precinctIcon.setImageURL("http://chart.apis.google.com/chart?chst=d_simple_text_icon_left&chld=|10|FFFFFF|civic-building|24|FFFFFF|000000");
					precinctIcon.setShadowURL(null);
					precinctIcon.setIconSize(Size.newInstance(30, 24));
					precinctIcon.setShadowSize(Size.newInstance(0, 0));
					precinctIcon.setIconAnchor(Point.newInstance(8, 8));
					maps2.addOverlay(guess);
					forZoomClearing2.add(guess);
					done.run();
					return;
				}
				Log.debug("Acc:" + best.getAccuracy());
			} else {
				finalBounds = bounds;
			}
			trySearchingAgain(finalBounds);
		}

		@Override
		public void onFailure(int statusCode) {
			if (statusCode < 600 || statusCode > 604) {
				// Fatal Codes -- don't retry
				Log.debug("Failed FATALLY.  Status:" + statusCode);
				UIUtils.error(errorLabel2, "Failed.");
				done.run();
				return;
			}
			if (bounds == null) {
				// Try without the last specifier
				Log.debug("Not even the city was found. :(");
				UIUtils.error(errorLabel2, "Failed in looking for the city/municipality.");
			} else {
				// Search but with one less
				trySearchingAgain(bounds);
			}
		}

		public void trySearchingAgain(final LatLngBounds finalBounds) {
			if (iteration == 0) {
				Log.debug("Ran out of iterations.");
				UIUtils.error(errorLabel2, "Not found. Please improve the map to have it.");
			}
			// Try with the nextAddress (remove last component)
			String newAddress = nextAddress;
			if (bounds != null) {
				if (newAddress.lastIndexOf(",") != -1) {
					newAddress = newAddress.substring(0, newAddress.lastIndexOf(","));
				} else {
					done.run();
					Log.debug("Ran out of commas.");
					UIUtils.error(errorLabel2, "Not found. Please improve the map to have it.");
					return;
				}
			}
			final String finalAddress = newAddress;
			Log.debug("Running: " + newAddress);
			Timer time = new Timer() {

				@Override
				public void run() {
					Geocoder coder = new Geocoder();
					coder.getLocations(finalAddress, new SmartLocationCallback(maps2, finalAddress,
							SmartLocationCallback.this.bestLocation, finalBounds, done,
							iteration - 1, forZoomClearing2, errorLabel2));
				}
			};
			time.schedule(1000);
		}
	}

	@UiHandler("submitLocation")
	void onSubmitLocation(ClickEvent e) {
		if (precinctMarker == null || finalizingLocation) {
			return;
		}
		finalizingLocation = true;
		submitLocation.addStyleName("disabled");
		spinnerImage.setVisible(true);
		loadingLabel.setText("Saving contribution...");

		PrecinctServiceAsync service = (PrecinctServiceAsync) GWT.create(PrecinctService.class);

		service.addAddressGeoLocation(addressInfo,
				precinctMarker.getLatLng().getLatitude(),
				precinctMarker.getLatLng().getLongitude(),
				new AsyncCallback<Void>() {

					@Override
					public void onSuccess(Void result) {
						clearWaiting();
						Util.track("/PrecinctFinder/09-Saved");
						errorLabel.setText("Your contribution was saved!!");
						(new Timer() {
							@Override
							public void run() {
								resetMap();
								breadCrumb.popEntry();
							}
						}).schedule(2000);
					}

					@Override
					public void onFailure(Throwable caught) {
						clearWaiting();
						Util.track("/PrecinctFinder/09-Saved-Fail");
						UIUtils.error(errorLabel, "Saving failed.  Please try again later.");
					}

					private void clearWaiting() {
						finalizingLocation = false;
						loadingLabel.setText("");
						spinnerImage.setVisible(false);
						submitLocation.removeStyleName("disabled");
					}

			});
	}

	public void resetMap() {
		maps.removeMapClickHandler(clickHandler);
		maps.removeMapZoomEndHandler(zoomHandler);
		maps.clearOverlays();
		maps.setWidth("400px");
		while (maps.getZoomLevel() > 15)
			maps.zoomOut();
	}

}
